// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FS_PSEUDO_DIR_H_
#define FS_PSEUDO_DIR_H_

#include <fbl/intrusive_wavl_tree.h>
#include <fbl/macros.h>
#include <fbl/mutex.h>
#include <fbl/ref_counted.h>
#include <fbl/string.h>
#include <fbl/unique_ptr.h>

#include "vnode.h"
#include "watcher.h"

namespace fs {

// A pseudo-directory is a directory-like object whose entries are constructed
// by a program at runtime.  The client can lookup, enumerate, and watch these
// directory entries but it cannot create, remove, or rename them.
//
// This class is designed to allow programs to publish a relatively small number
// of entries (up to a few dozen) such as services, file-system roots,
// debugging pseudo-files, or other vnodes.  It is not suitable for very large
// directories (hundreds of entries).
//
// This class is thread-safe.
class PseudoDir : public Vnode {
 public:
  // Creates a directory which is initially empty.
  PseudoDir();

  // Destroys the directory and releases the nodes it contains.
  ~PseudoDir() override;

  // Adds a directory entry associating the given |name| with |vn|.
  // It is ok to add the same Vnode multiple times with different names.
  //
  // Returns |ZX_OK| on success.
  // Returns |ZX_ERR_ALREADY_EXISTS| if there is already a node with the given name.
  zx_status_t AddEntry(fbl::String name, fbl::RefPtr<fs::Vnode> vn);

  // Removes a directory entry with the given |name|.
  //
  // Returns |ZX_OK| on success.
  // Returns |ZX_ERR_NOT_FOUND| if there is no node with the given name.
  zx_status_t RemoveEntry(fbl::StringPiece name);

  // An extension of |RemoveEntry| which additionally verifies
  // that the target vnode is |vn|.
  //
  // Returns |ZX_OK| on success.
  // Returns |ZX_ERR_NOT_FOUND| if there is no node with the given name/vn
  // pair.
  zx_status_t RemoveEntry(fbl::StringPiece name, fs::Vnode* vn);

  // Removes all directory entries.
  void RemoveAllEntries();

  // Checks if directory is empty.
  // Be careful while using this function if using this Dir in multiple
  // threads.
  bool IsEmpty() const;

  // |Vnode| implementation:
  zx_status_t Open(uint32_t flags, fbl::RefPtr<Vnode>* out_redirect) final;
  zx_status_t Getattr(vnattr_t* a) final;
  zx_status_t Lookup(fbl::RefPtr<fs::Vnode>* out, fbl::StringPiece name) final;
  void Notify(fbl::StringPiece name, unsigned event) final;
  zx_status_t WatchDir(fs::Vfs* vfs, uint32_t mask, uint32_t options, zx::channel watcher) final;
  zx_status_t Readdir(vdircookie_t* cookie, void* dirents, size_t len, size_t* out_actual) final;
  bool IsDirectory() const final { return true; }
  zx_status_t GetNodeInfo(uint32_t flags, fuchsia_io_NodeInfo* info) final;

 private:
  static constexpr uint64_t kDotId = 1u;

  struct NameTreeTraits;
  struct IdTreeTraits;
  class Entry {
   public:
    Entry(uint64_t id, fbl::String name, fbl::RefPtr<fs::Vnode> node);
    ~Entry();

    uint64_t id() const { return id_; }
    const fbl::String& name() const { return name_; }
    const fbl::RefPtr<fs::Vnode>& node() const { return node_; }

   private:
    uint64_t const id_;
    fbl::String name_;
    fbl::RefPtr<fs::Vnode> node_;

    // Node states.
    friend IdTreeTraits;
    friend NameTreeTraits;
    fbl::WAVLTreeNodeState<fbl::unique_ptr<Entry>> id_tree_state_;
    fbl::WAVLTreeNodeState<Entry*> name_tree_state_;
  };

  struct KeyByIdTraits {
    static uint64_t GetKey(const Entry& entry) { return entry.id(); }
    static bool LessThan(uint64_t key1, uint64_t key2) { return key1 < key2; }
    static bool EqualTo(uint64_t key1, uint64_t key2) { return key1 == key2; }
  };

  struct KeyByNameTraits {
    static fbl::String GetKey(const Entry& entry) { return entry.name(); }
    static bool LessThan(const fbl::String& key1, const fbl::String& key2) { return key1 < key2; }
    static bool EqualTo(const fbl::String& key1, const fbl::String& key2) { return key1 == key2; }
  };

  struct IdTreeTraits {
    using PtrTraits = fbl::internal::ContainerPtrTraits<fbl::unique_ptr<Entry>>;
    static fbl::WAVLTreeNodeState<fbl::unique_ptr<Entry>>& node_state(Entry& entry) {
      return entry.id_tree_state_;
    }
  };

  struct NameTreeTraits {
    using PtrTraits = fbl::internal::ContainerPtrTraits<Entry*>;
    static fbl::WAVLTreeNodeState<Entry*>& node_state(Entry& entry) {
      return entry.name_tree_state_;
    }
  };

  using EntryByIdMap = fbl::WAVLTree<uint64_t, fbl::unique_ptr<Entry>, KeyByIdTraits, IdTreeTraits>;
  using EntryByNameMap = fbl::WAVLTree<fbl::String, Entry*, KeyByNameTraits, NameTreeTraits>;

  mutable fbl::Mutex mutex_;

  uint64_t next_node_id_ __TA_GUARDED(mutex_) = kDotId + 1;
  EntryByIdMap entries_by_id_ __TA_GUARDED(mutex_);
  EntryByNameMap entries_by_name_ __TA_GUARDED(mutex_);

  fs::WatcherContainer watcher_;  // note: uses its own internal mutex

  DISALLOW_COPY_ASSIGN_AND_MOVE(PseudoDir);
};

}  // namespace fs

#endif  // FS_PSEUDO_DIR_H_
